Part V
MFC Utility Classes

In This Part

  Strings and Collections 731
  File I/O and MFC 757
  Exceptions 781

Chapter 20
Strings and Collections

by K. David White

In This Chapter

  Strings and String Classes 732
  Collections 741
  The UNL_MultiEd Application 749
  An STL Approach 754

Character manipulation and managing data in lists and sets have been among the staples of software development since the beginning of “computerdom.” In fact, most of you have probably grown weary of the “Hello World” program in all its varied forms. It seems as if you can’t pick up a beginner’s programming book, in any language, without finding it.

I don’t have the heart to do that to you, and because this is not a beginner’s book, you’ve probably been through it. You might have even used CString in some fashion, or one of the templated list classes that MFC provides. In this chapter, you will take a closer look at what MFC gives you as far as managing strings, lists, sets, and maps.

Without going into much detail, I will also introduce you to the Standard Template Library (STL) functions that you might end up using in place of the MFC classes. Remember, you may be asked to develop COM objects without using the MFC DLLs. Understanding what MFC gives you, and then taking a look at other alternatives, only helps to expand your capabilities.

Strings and String Classes

In Fortran, you were introduced to the concept of a string in all its formatted glory. If you haven’t programmed in Fortran, you haven’t lived! In C, you were introduced to character strings. These were treated differently, even though they did the same thing. In fact, a big part of your learning experience with C was learning to deal with character pointers. I don’t have a fond memory of those days, only the day that it finally sunk in and the world opened up! (Well, almost!)

C++, with the object-oriented approach, still makes use of character strings, but people started encapsulating string functions into string classes that could be used easily. Well, MFC does the same thing. The CString class encapsulates character string functionality to make string manipulation a breeze.

Inside the CString Class

The CString class is defined in the MFC header file AFX.H. If you look inside this file for the CString declaration, you will notice that it goes on for quite a few pages. The main element, though, is the buffer that CString encapsulates, as you see in Listing 20.1.

Listing 20.1 The CString Protected Data Member (AFX.H)


protected:
    LPTSTR m_pchData;   // pointer to ref counted string data

Listing 20.1 shows the only data member of CString, m_pchData. It makes sense! CString encapsulates a single string, or character buffer. The LPTSTR is defined as a 32-bit pointer to a character string. Now that you know CString contains a string pointer, you probably have questions about memory allocation and reallocation and where that buffer is actually kept. Are you ready to look at some MFC code to solve this mystery?

Notice the comment in Listing 20.1, // pointer to ref counted string data. This is a pointer to a reference-counted string. What exactly does it mean to have reference counting on a string? To solve the problem of memory leaks, and having copies of strings floating around that are no longer used, MFC implements a form of reference counting to verify that no unnecessary copies of the buffer are hanging around. If you’ve read my COM chapter (Chapter 10), you understand the concept of reference counting. Notice in Listing 20.2, which is an expanded listing of the CString declaration, that you don’t have anything to store the reference count.

Listing 20.2 The Expanded CString Declaration (AFX.H)


// Implementation
public:
    ~CString();
    int GetAllocLength() const;
    protected:
    LPTSTR m_pchData;   // pointer to ref counted string data
        // implementation helpers
    CStringData* GetData() const;
    void Init();
    void AllocCopy(CString& dest, int nCopyLen,
    Äint nCopyIndex, int nExtraLen) const;
        void AllocBuffer(int nLen);
        void AssignCopy(int nSrcLen, LPCTSTR lpszSrcData);
        void ConcatCopy(int nSrc1Len, LPCTSTR lpszSrc1Data,
        Äint nSrc2Len, LPCTSTR lpszSrc2Data);
        void ConcatInPlace(int nSrcLen, LPCTSTR lpszSrcData);
        void CopyBeforeWrite();
        void AllocBeforeWrite(int nLen);
        void Release();
        static void PASCAL Release(CStringData* pData);
        static int PASCAL SafeStrlen(LPCTSTR lpsz);
        static void FASTCALL FreeData(CStringData* pData);

You will notice that many functions are defined that appear to do the reference counting for the CString buffer, but it still doesn’t have a reference count holder. Notice the line CStringData* GetData() const;. This declaration is a clue! Back to AFX.H. Listing 20.3 shows you that the CStringData structure contains the reference count and string information important to the other CString functions. You will also noti that MFC used to store the data buffer here as well.

Listing 20.3 The CStringData Structure (AFX.H)


struct CStringData
{
    long nRefs;             // reference count
    int nDataLength;        // length of data (including terminator)
           int nAllocLength;       // length of allocation
           // TCHAR data[nAllocLength]

           TCHAR* data()           // TCHAR* to managed data
               { return (TCHAR*)(this+1); }
       };

String Allocation

The CString class uses the AllocBuffer member helper function to allocate and manage the memory during the construction phase. Listing 20.4 is the source code for the buffer allocation logic. Notice the comment starting at the second line, // always allocate one extra character for ‘\0’ termination. This allocation will automatically add the null terminator, so remember to send the actual length of the string that you need to create. If the length passed is zero, which is the case during a declaration without assignment, this routine will call the Init() routine to reserve some space. Okay, what are the following lines doing?

pData = (CStringData*)
    new BYTE[sizeof(CStringData) + (nLen+1)*sizeof(TCHAR)];
pData->nAllocLength = nLen;



It looks as if the allocation adds the length of the desired string to the length of a CStringData helper. At first glance, this might be confusing, but remember that CStringData maintains the reference count for the string buffer.

Listing 20.4 The CStringData Structure (AFX.H)


void CString::AllocBuffer(int nLen)
// always allocate one extra character for ‘\0’ termination
// assumes [optimistically] that data length will equal
// allocation length
{
    ASSERT(nLen >= 0);
    ASSERT(nLen <= INT_MAX-1);    // max size (enough room
                                  // for 1 extra)

    if (nLen == 0)
        Init();
    else
    {
        CStringData* pData;
#ifndef _DEBUG
        if (nLen <= 64)
        {
            pData = (CStringData*)_afxAlloc64.Alloc();
            pData->nAllocLength = 64;
        }
        else if (nLen <= 128)
        {
            pData = (CStringData*)_afxAlloc128.Alloc();
            pData->nAllocLength = 128;
        }
        else if (nLen <= 256)
        {
            pData = (CStringData*)_afxAlloc256.Alloc();
            pData->nAllocLength = 256;
        }
        else if (nLen <= 512)
        {
            pData = (CStringData*)_afxAlloc512.Alloc();
            pData->nAllocLength = 512;
        }
        else
#endif
        {
            pData = (CStringData*)
                new BYTE[sizeof(CStringData) + (nLen+1)* sizeof(TCHAR)];
            pData->nAllocLength = nLen;
        }
        pData->nRefs = 1;
        pData->data()[nLen] = ‘\0’;
        pData->nDataLength = nLen;
        m_pchData = pData->data();
    }
}

I think that reference counting for strings deserves a closer look because it is evident that each buffer carries with it a reference count. Suppose that the MFC CString class didn’t maintain a pointer to a buffer, but contained the buffer itself. Each time a CString was created from another CString, it would get a copy of that buffer. With the MFC implementation, the new CString will be created to point at the same data buffer and will maintain its pointer to that buffer. Meanwhile, back in CStringData, the reference count gets bumped, indicating that another allocation of the buffer is active. That way, if the original CString gets “blown away” (a common occurrence), the buffer for the copied CString still has the buffer. When there are no more CStrings pointing to the same buffer, its reference count becomes 0, and it can go away peacefully—no more memory leaks!

I implore you to investigate the CString code to learn more about how it really works. You might someday thank me! Friends of mine, doing things not meant to be done with MFC, have had to roll their own string class and have paid some dues with reference counting and memory allocation.


Tip:  

Assigning a CString to be a single character, although supported, is not really a wise choice. The CString allocation and reference counting for a single character is decidedly an unnecessary thing to do.


Some CString Functions

Before you walk away from this discussion about what makes up the CString, I think you should come with me on a tour of some of the more interesting functions. By finding out how CString encapsulates its functionality, you might be better able to develop without it if the need ever arises.

Concatenation

It is common practice to concatenate one string with another. This is primarily done with the concatenate operator (+=), which is defined in AFX.INL. Listing 20.5 shows the concatenate operator.

Listing 20.5 The CString Concatenate Operator (AFX.INL)


#ifdef _UNICODE
_AFX_INLINE const CString& CString::operator+=(char ch)
     { *this += (TCHAR)ch; return *this; }
_AFX_INLINE const CString& CString::operator=(char ch)
     { *this = (TCHAR)ch; return *this; }
_AFX_INLINE CString AFXAPI operator+(const CString& string, char ch)
     { return string + (TCHAR)ch; }
_AFX_INLINE CString AFXAPI operator+(char ch, const CString& string)
     { return (TCHAR)ch + string; }
 #endif

Notice in each of these overloaded operators the use of TCHAR. TCHAR is defined as an 8-bit ANSI character unless it is defined as a Unicode character. If it is a Unicode character, it is defined as a 16-bit Unicode character. Notice that there are many different forms of concatenation. You can add single characters or another CString.

Searching a CString

There are two primary functions when it comes to searching a CString for characters or smaller substrings. The Find function will find the search string in a forward direction in the buffer. The ReverseFind function will do the exact opposite. Listing 20.6 contains the code for the Find functions from STRCORE.CPP. Listing 20.7 contains the ReverseFind implementation, which resides in STREX.CPP. Notice from the comment in the second line of the STRCORE.CPP listing, // Commonly used routines (rarely used routines in STREX.CPP), that the rarely used routines are implemented in STREX.CPP.

Listing 20.6 The CString Find Functions (STRCORE.CPP)


/////////////////////////////////////////////////////////////////////
// Commonly used routines (rarely used routines in STREX.CPP)

int CString::Find(TCHAR ch) const
{
    return Find(ch, 0);
}

int CString::Find(TCHAR ch, int nStart) const
{
    int nLength = GetData()->nDataLength;
    if (nStart >= nLength)
        return -1;

    // find first single character
    LPTSTR lpsz = _tcschr(m_pchData + nStart, (_TUCHAR)ch);

    // return -1 if not found and index otherwise
    return (lpsz == NULL) ? -1 : (int)(lpsz - m_pchData);
}

int CString::FindOneOf(LPCTSTR lpszCharSet) const
{

ASSERT(AfxIsValidString(lpszCharSet));
    LPTSTR lpsz = _tcspbrk(m_pchData, lpszCharSet);
    return (lpsz == NULL) ? -1 : (int)(lpsz - m_pchData);
}

Listing 20.7 The CString ReverseFind Function (STREX.CPP)


/////////////////////////////////////////////////////////////////////
// Finding

int CString::ReverseFind(TCHAR ch) const
{
    // find last single character
    LPTSTR lpsz = _tcsrchr(m_pchData, (_TUCHAR) ch);

    // return -1 if not found, distance from beginning otherwise
    return (lpsz == NULL) ? -1 : (int)(lpsz - m_pchData);
}

In both listings, you should notice the use of _tcschr (or a related function, such as _tcspbrk). This is one of many string functions that is similar to the C runtime library functions for manipulating character strings. Okay, you’ve come full circle. I started off by explaining that MFC provides this really neat class that handles the character string stuff for you. Well, MFC encapsulates the exact functionality that you were using in C.

Comparing CStrings

The CString class provides two methods of comparing strings. These are the Compare() member function and the == operator. In fact, if you look at the MFC code, these functions are essentially identical. These functions also use the _t string manipulation functions for comparing strings.



Practical CString Usage

Your brain is probably buzzing from the MFC code that I’ve given you, but let’s put some of this newfound knowledge to work. Listing 20.8 is from the UNL_MultiEd application (the SplitPartsString utility function found in cx_eventrecorder.cpp). This application is explained in better detail at the end of this chapter.

Listing 20.8 The UNL_MultiEd SplitPartsString Utility


 //******************************************************************
 // This utility function takes a delimited string and splits
 // parsed strings based on the delimiter.  Useful for parsing
 // COMMA delimited strings...
 //
 // NOTE:   This is strictly the MFC version.
 //
void
SplitPartsString( const CString&   cr_sLine,
                  CStringList&     r_lParts,
                  const CString&   cr_sDelimiter)
{
    CString  sPart          = “”;
    CString  sTemp;
    int      iLineLength = 0;
    int      iCurrentPos = 0;
    int      iTmpPos     = 0;
    r_lParts.RemoveAll();
    sTemp =  cr_sLine;
    iLineLength = sTemp.GetLength();
    /*********************************
    **      Main parsing loop..     **
    *********************************/
    while (0 != iLineLength)
    {
         /**************************************************
         **  You should have a delimiter in this string   **
         **  ...if not, then jump to else loop comment    **
         **************************************************/
         if (-1 != (iTmpPos = sTemp.Find(cr_sDelimiter)))
         {
             iCurrentPos = iTmpPos;

             sPart = sTemp.Left(iCurrentPos);

             if (CString(“\t”) ==
            ÄsTemp.Right(iLineLength - iCurrentPos))
             {
                 sTemp += “ ”iLineLength++;
             }
             sTemp = sTemp.Right(iLineLength - iCurrentPos - 1);

             //  You have a clean partial string.. add it to
             // the StringList to pass back.
             r_lParts.AddTail(sPart);
         }
         else
         {
             //
             // Okay... You are out of delimiters in the main
             // string.  Make the rest of the string the
             // last part
             //
             sPart = sTemp; // Set remaining part.

             sTemp = sEMPTY_STRING;

             if (sEMPTY_STRING != sPart)
             {
                 // Strip the \255 character that

                 // the MFC_CSTR_KLUDGE macro inserted
                 // (MFC StdioFile::ReadString buffer bug...)
                 if (-1 != (iTmpPos = sPart.Find(‘\255’)))
                     sPart = sPart.Left(iTmpPos-1);
                 r_lParts.AddTail(sPart);
             }
         }
    }
}

This routine will take a string read in from a file and will split it into several strings. The main string contains a delimiter that will be used to separate the string. Later in this chapter, when I introduce you to collection classes, I’ll explain the CStringList collection class. I’ve included the entire routine here because it will be referenced at that point.

I want to point out the general CString functions that every developer should become infinitely familiar with. First of all, look at the following lines:

CString  sPart          = “”;
CString  sTemp;

Remember the allocation discussion. In the first of these lines, you did the allocation via an equate. It could be done as follows:

CString sPart(“”);

Either way is acceptable and is simply a matter of preference, but the allocation discussion still holds. Remember what I said about reserving an unknown amount that would later be allocated and reference counted when an assignment was performed? The string will get allocated later when the assignment is done.

The GetLength() function member will return the number of characters minus the null terminator character. The Find() function member is used to locate the delimiter being used to separate the substrings.

This routine also contains the substring functions, which will return substrings of the main string. When I first started using these functions, I had countless hours of frustration. The Left() function will return a subset of the string from the length passed into as an argument. The line sPart = sTemp.Left(iCurrentPos); will receive the subset of the string up to the position of the first delimiter—your first part! The line

sTemp = sTemp.Right(iLineLength - iCurrentPos - 1);

will return the remainder of the string for further parsing.

Although they don’t exist in this routine, another popular CString feature set that is of interest is the Trim() functions. The TrimRight() function will remove whitespace characters from the end of the string. If a character is entered as the parameter, TrimRight() will remove everything to the right of that character, including that character. Be careful here! If another CString is entered as the parameter, it will remove everything to the right of that substring, including the substring. TrimLeft() will trim off leading whitespace characters from the string. The same parameter functionality applies in reverse here.


Note:  

A whitespace character can be defined as a space, newline, or tab character.


Although I haven’t provided every possible example of what you can do via the CString class, I’m sure you’ll agree that it’s pretty flexible. Spend some time writing some code to use all the functions of the CString class, and you will soon appreciate its features.

CString Summary

You might be thinking that a tour of the MFC seems to simply cloud the mind with useless information. I disagree. I feel that if you understand how something works, implementing it becomes easier. I hope that you will search the MFC code archives for how the CString class is put together and spend some time understanding how it does its allocation. Understanding when and how to implement CString versus some other mechanism is an essential coding practice.

The example, although quite simple, is a very good representation of much of what goes on when implementing CString code. Other examples are available through the samples in this book and on many Web sites that contain information about MFC coding.

Now, on to collections.

Collections

What are collections? A collection can be defined as a set of like or unique items that are collectively rendered. Linked lists come to mind when discussing collections, and you will soon find out that MFC provides a very usable set of collection classes to deal with data collection problems. Before discussing the collections themselves, I should probably point out something that you are currently thinking about. A collection is defined by what it is storing. MFC gives you a set of data structures that store the application’s data. For each of these shapes or structures, MFC provides non-templated classes to hold the data. BYTE, int, and WORD are just a few. MFC also provides templated versions of each of these structures that you can use to create collections.

Table 20.1 defines the list of MFC-provided template classes that allow you to work with application data.

Table 20.1 Collection Classes

Name Structure

CObList List
CPtrList List
CStringList List
CByteArray Array
CDWordArray Array
CObArray Array
CStringArray Array
CWordArray Array
CUIntArray Array
CMapPtrToPtr Map
CMapStringToPtr Map
CMapWordToPtr Map
CMapStringToString Map
CmapStringToOb Map
CMapWordToOb Map



Inside Collection Classes

Time to look inside at what MFC is really doing, with a look at allocation issues. The non-templated arrays and list are defined in AFXCOLL.H. There is behavior that is similar between the different collection classes, but there are also unique implementation details that deserve some level of attention.

The Array Type Collections

Taking a look at Listing 20.9 provides some insight as to the functionality that is encapsulated in these classes. Although this is only one of the many types available, it will paint a picture for the remainder of the classes.

Listing 20.9 The CByteArray Definition (AFXCOLL.H)


class CByteArray : public CObject
{

    DECLARE_SERIAL(CByteArray)
public:

// Construction
    CByteArray();

// Attributes
    int GetSize() const;
    int GetUpperBound() const;
    void SetSize(int nNewSize, int nGrowBy = -1);

// Operations
     // Clean up
    void FreeExtra();
    void RemoveAll();

    // Accessing elements
    BYTE GetAt(int nIndex) const;
    void SetAt(int nIndex, BYTE newElement);

    BYTE& ElementAt(int nIndex);

    // Direct Access to the element data (may return NULL)
    const BYTE* GetData() const;
    BYTE* GetData();

    // Potentially growing the array
    void SetAtGrow(int nIndex, BYTE newElement);

    int Add(BYTE newElement);

    int Append(const CByteArray& src);
    void Copy(const CByteArray& src);

    // overloaded operator helpers
    BYTE operator[](int nIndex) const;
    BYTE& operator[](int nIndex);

    // Operations that move elements around
    void InsertAt(int nIndex, BYTE newElement, int nCount = 1);

    void RemoveAt(int nIndex, int nCount = 1);
    void InsertAt(int nStartIndex, CByteArray* pNewArray);

// Implementation

protected:
    BYTE* m_pData;   // the actual array of data
    int m_nSize;     // # of elements (upperBound - 1)
    int m_nMaxSize;  // max allocated
    int m_nGrowBy;   // grow amount

public:
    ~CByteArray();

    void Serialize(CArchive&);
#ifdef _DEBUG
    void Dump(CDumpContext&) const;
    void AssertValid() const;
#endif

protected:
    // local typedefs for class templates
    typedef BYTE BASE_TYPE;
    typedef BYTE BASE_ARG_TYPE;
};

At first glance, you notice functions in this class that you would expect for any array. Functions such as RemoveAll(), GetAt(), and SetAt() are self-explanatory. These functions manipulate the collection, in this case an array of BYTES. The data members are defined in the following lines:

BYTE* m_pData;   // the actual array of data
int m_nSize;     // # of elements (upperBound - 1)
int m_nMaxSize;  // max allocated
int m_nGrowBy;   // grow amount

Notice the last two lines. Do you think these play a part in memory allocation for the array? Absolutely!

There are two data members that refer to the size. The m_nSize data member refers to the actual size, and the m_nMaxSize is the reserved allocated space for the array. The m_nGrowBy data member indicates the number of elements to allocate in each allocation chunk. The array grows by chunks of elements, not necessarily by one element—unless, of course, the chunk is defined to be one element.

Listing 20.10 is a listing for the SetSize() function for the CByteArray. This is found in ARRAY_B.CPP. Notice in the line if (nNewSize == 0) that the first thing done is to see whether the array is to be deallocated. At this point, the users entered a new size of 0 instead of calling the destructor. If you are on your toes, you should catch this. The CByteArray class encapsulates the array, and by deallocating the array inside the class, it can be reallocated later without having to reconstruct the CByteArray class.

Listing 20.10 The CbyteArray::SetSize Function (ARRAY_B.CPP)


void CByteArray::SetSize(int nNewSize, int nGrowBy)
{
    ASSERT_VALID(this);
    ASSERT(nNewSize >= 0);
        if (nGrowBy != -1)
        m_nGrowBy = nGrowBy;  // set new size
        if (nNewSize == 0)
    {
        // shrink to nothing
        delete[] (BYTE*)m_pData;
        m_pData = NULL;
        m_nSize = m_nMaxSize = 0;
    }
    else if (m_pData == NULL)
    {
        // create one with exact size
#ifdef SIZE_T_MAX
        ASSERT(nNewSize <= SIZE_T_MAX/sizeof(BYTE)); // no overflow
#endif
        m_pData = (BYTE*) new BYTE[nNewSize * sizeof(BYTE)];
        memset(m_pData, 0, nNewSize * sizeof(BYTE));  // zero fill
        m_nSize = m_nMaxSize = nNewSize;
    }
    else if (nNewSize <= m_nMaxSize)
    {
        // it fits
        if (nNewSize > m_nSize)
        {
            // initialize the new elements

            memset(&m_pData[m_nSize], 0,
            Ä(nNewSize-m_nSize) * sizeof(BYTE));

        }
        m_nSize = nNewSize;
    }
    else
    {
        // otherwise, grow array
        int nGrowBy = m_nGrowBy;
        if (nGrowBy == 0)
        {
            // heuristically determine growth when nGrowBy == 0
            //  (this avoids heap fragmentation in many cases)
            nGrowBy = min(1024, max(4, m_nSize / 8));
        }

        int nNewMax;
        if (nNewSize < m_nMaxSize + nGrowBy)
            nNewMax = m_nMaxSize + nGrowBy;  // granularity
        else
            nNewMax = nNewSize;  // no slush

        ASSERT(nNewMax >= m_nMaxSize);  // no wrap around
#ifdef SIZE_T_MAX
        ASSERT(nNewMax <= SIZE_T_MAX/sizeof(BYTE)); // no overflow
#endif
        BYTE* pNewData = (BYTE*) new BYTE[nNewMax * sizeof(BYTE)];
        // copy new data from old
        memcpy(pNewData, m_pData, m_nSize * sizeof(BYTE));
        // construct remaining elements
        ASSERT(nNewSize > m_nSize);
        memset(&pNewData[m_nSize], 0,
        Ä(nNewSize-m_nSize) * sizeof(BYTE));


        // get rid of old stuff (note: no destructors called)
        delete[] (BYTE*)m_pData;
        m_pData = pNewData;
        m_nSize = nNewSize;
        m_nMaxSize = nNewMax;
    }
}

The line else if (m_pData == NULL) tests for the condition where the array data is empty, and the SetSize function has been called to allocate a chunk of memory. Notice the line memset(m_pData, 0, nNewSize * sizeof(BYTE)); // zero fill. The allocation is done via the memset function. After looking at this code, you should realize just how simple and elegant some MFC classes actually are.

The List Type Collections

An array defines a bounded collection of like items. A list is similar to a single bounded array. MFC encapsulates this functionality in much the same way. The primary difference here is that a list contains pointers to indicate position in the list, as opposed to indexes into an array. With MFC, you get both Head pointers and Tail pointers.

The POSITION pointer is an abstract datatype used to control iteration of a list. There are functions that will return the POSITION of the Head and Tail pointer that you can use to insert, remove, and copy list elements.

Because the memory allocation follows similar paths to that of the arrays, I won’t bog you down with MFC source code. The following are list functions that are useful to controlling your list data:

  GetHead()—Returns the Head pointer POSITION.
  GetTail()—Returns the Tail pointer POSITION.
  AddHead()—Adds the data element to the front of the list.
  AddTail()—Adds the data element to the back of the list.
  GetCount()—Returns the number of elements in the list.
  GetNext()—Returns the POSITION of the next element in the list.
  IsEmpty()—Returns TRUE if the list is empty.
  SetAt()—Modifies element at POSITION.
  GetAt()—Returns the element at POSITION.
  RemoveAll()—Removes all elements from the list.
  RemoveHead()—Removes the element located at the Head pointer.
  RemoveTail()—Removes the element at the Tail pointer.
  InsertAfter()—Inserts an element after the element pointed to by the POSITION iterator.
  InsertBefore()—Inserts an element before the element pointed to by the POSITION iterator.



Map Type Collections

You can think of a map as a hash table, with key/element pairing. That key can take many forms, and its simplest form would represent an array bound. I said this to set the stage for a discussion of maps and hashing the map. In an array, the elements are stored in sequential fashion, with sequential indexes. This is not always true for a Map. The associated key for the mapped element might not be numerical, or there could be gaps if it is numerical.

Hashing refers to the practice of generating an index from a key. An algorithm, or simple function, referred to as a hashing function, is applied to the key to produce a unique lookup index to the element desired. When adding elements to a map, you generate the index by passing in the key for the element. When you need to retrieve the element, you pass the key into the same hash function and then use the returned index to find the data.


Tip:  

If the data collection is to be quite large, you should consider using the Map classes. Iterating a large list or array can be costly.


MFC associates items stored in the CMap classes using a simple yet effective hash key generation algorithm. Listing 20.11 shows you the CMapStringToString hashing implementation. (Note that it is inline to further increase its efficiency.) This is representative of all map hashing algorithms. You also can see how the hash table is initialized, which is also quite efficient.

Listing 20.11 The CMapStringToString Associative Hashing Function and Hash Table Initialization (MAP_SS.CPP)


inline UINT CMapStringToString::HashKey(LPCTSTR key) const
{
    UINT nHash = 0;
    while (*key)
        nHash = (nHash<<5) + nHash + *key++;
    return nHash;
}
    void CMapStringToString::InitHashTable(
    UINT nHashSize, BOOL bAllocNow)
//
// Used to force allocation of a hash table or to override the default
//   hash table size of (which is fairly small)
{
    ASSERT_VALID(this);
    ASSERT(m_nCount == 0);
    ASSERT(nHashSize > 0);
        if (m_pHashTable != NULL)
    {
        // free hash table
        delete[] m_pHashTable;
        m_pHashTable = NULL;
    }
        if (bAllocNow)
    {
        m_pHashTable = new CAssoc* [nHashSize];
        memset(m_pHashTable, 0, sizeof(CAssoc*) * nHashSize);
    }
    m_nHashTableSize = nHashSize;
}

As I mentioned previously, the hashing function is indeed an inline function (see the first seven lines of Listing 20.11), which is decidedly quicker than accessing typical CMap class methods. In this case, the string is iterated to determine the hashing value, based on content. Regarding initialization—note that the InitHashTable routine allocates the memory for the hash table to store its indexes. If one already exists, it is deleted. It then uses memset to allocate the memory.


Tip:  

Defining your own hash table size can also greatly increase performance.


Templated Collections

MFC provides a way for you to define your own collections. Some data is not always easily defined by the generic set. You can have user-defined classes that need to be kept in a list or array. These are defined in the AFXTEMPL.H file. For the sake of getting to practical application, I won’t list this for you.

The UNL_MultiEd Application

The UNL_MultiEd application, which is introduced in Chapter 7, “The Document/View Architecture,” is a good example of collections and strings. This section briefly details this application and provides listings of most items that pertain to strings and collections.

Overview

The application is an event management tool that will allow the user to define an event with multiple contests. The goal is to provide a list of participants and a list of events and to determine the overall winner. The user has the ability to enter events and participants into an ASCII-delimited file that can be loaded into the application. The data is kept globally and is pointed to by the application object.

The CXEventRecorder object contains a list of participants, a list of events, and an array that keeps a sorted list based on total points. Each participant has a running total of his or her points. Participants can be added to an event and can be placed in that event based on the outcome of that event.

This application would be a good starting point for someone who has to maintain the records for a decathlon, or any other event that has multiple contests (see Listing 20.12).

Listing 20.12 The CXEvent, CXParticipant, and CXEventRecorder Definitions


// xeventrecorder.h

#ifndef cxeventrecorder_h
#define cxeventrecorder_h

#include “stdafx.h”
#include <stdio.h>


#include <afxtempl.h>

//
// CXParticipant:
//
// This class stores information about the participants in the/
// Games. The constructors will insert the information.
// This class is added in the map for the EventRecorder class.
//

class CXParticipant
{
    public:

        CXParticipant();
        CXParticipant(CString sLastName);
        CXParticipant(CString sLastName, CString sFirstName);
        CXParticipant(CString sLastName, CString sFirstName,
        ÄCString sTeam);

        virtual ~CXParticipant();

        void SetFirstName(CString sFirstName);
        void SetLastName(CString sLastName);
        void SetTeam(CString sTeam);

        CString *GetFirstName();
        CString *GetLastName();
        CString *GetTeam();

        void AddPoints(int nPoints);
        void BumpEvent();

        int GetRunningTotal();
        int GetEventsEntered();

    private:

        CString     m_sLastName;
        CString     m_sFirstName;
        CString     m_sTeam;

        int         m_nEventsEntered;
        int         m_nRunningPoints;
};

//
// CXEvent:
//
// This class stores information about the event. However, it
// only stores whether participants competed.
//

class CXEvent
{
    public:

        CXEvent();
        virtual ~CXEvent();

    CXEvent&    operator = (const CXEvent& pOther);
    int        operator == (CXEvent *pOther);

        void SetEventName (CString sName);
        CString *GetEventName ();

        void SetEventRan (BOOL bEventRan);
        BOOL GetEventRan ();

        void AddParticipantToEvent(CXParticipant * eventParticipant);
        void RemoveParticipant(CXParticipant * eventParticipant);

        /*********************************************
        **  This method will place the participant  **
        **  in the event (First, Second, etc..)     **
        *********************************************/
        void PlaceParticipantInEvent (int nPos,
        ÄCXParticipant * ourParticipant);

    private:

        CString     m_sName;
        BOOL        m_bEventRan;

        CList <CXParticipant *, CXParticipant *> *m_slEventParticipants;
        CMap <int, int, CXParticipant *, CXParticipant *>
        Ä*m_mEventPlaces;
};


//
// CXEventRecorder Class.
//

**      This class is a global store for the games...     **
// Maps are kept of events and participants and event totals
// It also contains information regarding the reporting
// of the events.
class CXEventRecorder
{
    public:

    CXEventRecorder();
        virtual ~CXEventRecorder();

        /*********************************************
        **  This will take an ASCII delimited file  **
        **  containing the events to take place     **
        **  and load them into the Event Map..      **
        *********************************************/
        void LoadEvents(CString sFileName);
        /*********************************************
        **  This is called from LoadEvents and will **
        **  create the participants                 **
        *********************************************/
        void LoadParticipants ( CString sLastName,
                                CString sFirstName,
                                CString sTeam);
        /*********************************************
        **  This method will set the Event number   **
        **  that is currently being recorded.       **
        *********************************************/
        void SelectEvent(int EventNum);

        CList <CXEvent *, CXEvent *> *GetEvents ();
        CList <CXParticipant *, CXParticipant *> *GetParticipants ();

        void AddParticipant (CXParticipant * ourParticipant);
        void AddEvent(CXEvent * ourEvent);

        void RemoveParticipant (CXParticipant *ourParticipant);
        void RemoveEvent (CXEvent *ourEvent);

        /*********************************************
        **  Using the Running Totals structure..    **
        **  this routine will clear and then recalc **
        **  the m_TotalPlaceMap map                 **
        *********************************************/
        void CalculatePoints ();

    private:
        //
        // You need to keep a list of Events and a list of
        // Participants.  The Event class will place the
        // Participants in that event.  The m_plParticipants
        // map will be used to calculate overall winning
        // participant.
        //
        CList <CXParticipant*, CXParticipant *m_plParticipants;
        CList <CXEvent *, CXEvent *> *m_elEvents;

        CArray <CXParticipant*, CXParticipant*> *m_paTotalPoints;

        int     m_nActiveEvent;
};

#endif // cxeventrecorder_h



Notice that the CXEvent maintains a list of participants for that particular event. The EventDefineView is used to assign participants to an event. The EventTallyView is used to place the participants after their competition. The CalculatePoints() method of the CXEventRecorder will iterate the Events list, pull out Participants placement, and tally running totals for each participant. After this is done, it will iterate the list and find the top three overall contestants. See Listing 20.13 for a routine to add a participant to an event.

Listing 20.13 A Routine to Add a Participant to an Event


void
CXEvent::AddParticipantToEvent (CXParticipant * eventParticipant)
{
    POSITION pos = m_slEventParticipants->GetHeadPosition( );
    while( pos )
    {
        //Already in the list... Don’t do anything...
        if (m_slEventParticipants->GetNext(pos) == eventParticipant)
            return;
    }
    //  Ok... This isn’t a duplicate... add to our list...
    m_slEventParticipants->AddTail(eventParticipant);
}

The line POSITION pos = m_slEventParticipants->GetHeadPosition( ); defines the POSITION iterator to point to the front of the list. You could simply add a participant here because your view deletes from the available list when a participant is added to the event. However, it is good practice to verify that your list doesn’t have a duplicate. If there is no duplicate, you simply add the participant to the end of the list (see the next-to-last line of Listing 20.13). If the list needed to be sorted, it would be wise to apply a sorting algorithm and use the InsertAfter() or InsertBefore() member functions to enter participants into the list.

Returning to Listing 20.8 for a minute, you will notice the use of CStringList, which is not a templated list. Notice again that you are simply adding the part strings to the end of your string list. In most cases, your element insertion into a list will be at the tail or the head. See Listing 20.14 for a routine to remove a participant from an event.

Listing 20.14 A Routine to Remove a Participant from an Event


void
CXEvent::RemoveParticipant (CXParticipant * eventParticipant)
{
    POSITION pos = m_slEventParticipants->GetHeadPosition( );
    while( pos )
    {
        if (m_slEventParticipants->GetNext(pos) == eventParticipant)
            m_slEventParticipants->RemoveAt(pos);
    }
}

Here again, the code is quite simple. The list is iterated until the desired element is located and then removed via the RemoveAt() function. Notice that the RemoveAt() function takes the POSITION iterator.

An STL Approach

I would not want to leave this discussion without some mention of the Standard Template Library functions available for managing strings and lists. If you were tasked with the job of creating a lightweight ATL COM component that makes heavy use of strings and lists, would you know how?

To manage character string data, the STL provides the <string> templated class. In many respects, it is very similar to the CString class. I won’t go into extensive detail here, primarily because this is an MFC book. This string has operators that allow you to assign it and allocate memory. The buffer maintained in this template is available through the c_str() function.


Note:  

The string buffer available via c_str is not directly assignable to a CString value.


The following code snippet is an example of how you might define and use the string template class:

LPCSTR szTempString;
szTempString = (*it_sList).c_str();

When you have szTempString, you can then create or assign it to an MFC CString. The it_sList is a list iterator. The list is a string list.

Many functions available through the MFC CList class are also available through the STL template class, <list>. Refer to the online documentation for information on the STL template classes.

Summary

My wish is that you take this chapter as a beginning point for looking inside the MFC utility classes and learning how to effectively model your application’s data. The CString class, although powerful, can lead to many frustrating hours of memory allocation problems. Modeling your set data into a CList or a CMap, or some other collection class, can be a daunting task. The key to understanding these utility classes is first understanding your data.

I have presented a fairly robust, but purposely incomplete, sample application that is used not only in this chapter, but also in Chapter 7 and Chapter 21, “File I/O and MFC.” I have left the total calculations method up to you. This is a simple test to see whether you picked up anything about collections! The other item that is missing here is primarily for Chapter 21. There is no reporting mechanism for this application. You might want to display a report in HTML to a Web browser component and write that out to a file. The choices are endless. When you have a fun application with the two missing parts put together, email me, and we will compare approaches. I have the completed version and will be able to email you with that.